信号与CTRL+C

Ctrl+C最基本的了解

相信对于大部分人来说知道ctrl+c同时按下,当前进程会收到SIGINT信号,这个是最基本的,尤其是在执行自己编译的c程序,或者python进程的时候,想关闭的时候直接摁下就退出了,对吧:) ,接下来写几个shell来测试一下不同条件下的执行结果

1
2
3
4
5
6
7
#!/bin/bash
sleep 60 &
#这条命令的执行结果是直接退出sleep 60这个进程放后台执行,其父进程变为1,这个肯定都没有异议对吧
#[root@blog ~]sh test.sh
#[root@blog ~]ps -ef| grep sleep
#root 26871 1 0 00:03 pts/0 00:00:00 sleep 60
#root 26873 26529 0 00:03 pts/0 00:00:00 grep --color=auto sleep
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#如下为第二种情况
#!/bin/bash
sleep 60 &
wait #wait的意思是表示父进程等待其fork出去的子进程运行结束后才退出,意思就是这个脚本会在前台执行60s知道sleep 60退出

#[root@blog ~]sh test.sh


#[root@blog ~]
#[root@blog ~]
-#############################另外一个窗口########################
#[root@blog ~]ps -ef| grep sleep
#root 27449 27448 0 00:06 pts/0 00:00:00 sleep 60
#root 27453 27178 0 00:07 pts/3 00:00:00 grep --color=auto sleep
#可以看到父进程不退出的情况下sleep的父进程是不会发生切换的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#第三种情况两个脚本
#脚本一 sleep.sh
#!/bin/bash
sleep 60 & #考虑到只要父进程存在其ppid就不会变的情况,因此将&放置与此处,而不是在test.sh调用sleep.sh的地方,如果放在那里的话 sleep 与sleep.sh的进程关系不会改变,同时sleep.sh与test.sh的关系也不会变化,同上例
#而放在此处的话一旦运行起来就会有微妙的进程关系变化
#脚本二 test.sh
#!/bin/bash

sh sleep.sh
while((1)) #死循环确保父进程不退出,实验ctrl+c的效果
do
date
sleep 1
done
###这段代码测试发现:ctrl+C不会导致sleep 60进程的退出,kill -2 -pid也不能杀死子进程

信号

在linux中信号用以向一个进程发送某种信息告知进程某种情况发生了,可以理解成是一种相对低级基本的进程间通信。

常用的信号有15个,可以通过kill -l来将所有信号列出来,常用的kill默认发送的是15,该信号告知进程退出,所以一般来说kill 都会杀死进程,同时信号可以被捕捉到,否则会按照操作系统内核设置进行信号的处理,其中有一个例外,那就是-9信号,该信号不能被捕捉且进程会立刻退出(不会进行必要的清理工作),因此信号9不到迫不得已不推荐使用,一种最普遍的常景就是MySQL这种服务,在卡死无响应的时候如果使用-9强制杀死,那么一般的情况就是索引由于没有正常关闭,发生损坏。

在学习操作系统的时候书中有介绍,一般情况使用进程友好的15来杀死进程,如果进程需要对特定的信号进行处理可以使用操作系统预留给用户使用的SIGUSR2、SIGUSR1

nginx 就是使用自定义信号进行日志的rotate,以及二进制文件的热更新

常用信号

常用的信号除了有以上的SIGKILLSIGTERM之外目前还记得的比较特殊的就是SIGHUP,这个信号会挂起进程直至进程收到SIGCONT,当需要挂起暂停某个进程时,这两个信号尤其有用。

同时还有SIGABRT该信号会导致进程退出并产生core文件,如果没有记错的话,当进程发生段错误时就是由于操作系统像进程发送了该信号。印象中是这样

SIGINT与CTRL+C

正常情况下在shell中摁下ctrl+c的同时会向该进程组发送SIGINT,这里注意一下是进程组而不是像当前进程,这两个的区别就在于如果使用的是shell脚本产生了很多子进程,这种情况下如果子进程对SIGINT进行了捕获处理那么除了当前进程之外,其他进程也会退出。

这里可以写个代码验证一下,由于系统自带工具可能对信号做了处理,这里自己来编写测试,代码较少使用c实现,具体signal.h的使用可以在man中找到详细文档,这里不再赘述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>

int kp = 1;
void sig_handler(int i)//信号的handler
{
kp = 0;
printf("exiting...\n");
fflush(stdout);
exit(0);
}
int main(int argc,char *argv[])
{
struct sigaction act;
act.sa_handler = sig_handler;
sigaction(SIGINT,&act,NULL);//相对于signal该函数具有更好的移植性
while(kp)
{
printf("running...\n");
sleep(1);
fflush(stdout);//强制输出
}
}//文件编译后命名为catch_sig
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>

int kp = 1;
void sig_handler(int i)
{
kp = 0;
printf("exiting...\n");
fflush(stdout);
exit(0);
}
int main(int argc,char *argv[])
{
struct sigaction act;
act.sa_handler = sig_handler;
//sigaction(SIGINT,&act,NULL);
while(kp)
{
printf("running...\n");
sleep(1);
fflush(stdout);
}
}
//文件编译后命名为ignore_sig
1
2
3
4
5
6
7
8
9
10
11
12
13
#!/bin/env bash

#将如下三个命令放入后台
sleep 120 &
./ignore_sig &
./catch_sig &


while((1))
do
date
sleep 1
done

如下为执行结果,如果此时摁下ctrl+c可以看到test.sh退出,这个符合预期,同时catch_sig捕获到信号输出exiting。。。也输出了,执行ps可以看到sleep 120还在运行,同时向其发送kill -2无效,仍然运行

sh test.sh
running…
Tue Oct 24 15:30:52 UTC 2017
running…
running…
running…
Tue Oct 24 15:30:53 UTC 2017
running…
running…
Tue Oct 24 15:30:54 UTC 2017

^Cexiting…

[root@alarm clang]# running…
running…
running…
running…
running…
running…
running…
running…
running…
running…
running…

从上面的结果可以看出,ctrl+c验证了向进程组发送信号这件事。

细心地同学一定发现了一件事情,那就是既然ignore_sig sleep忽略信号SIGINT,那为啥在shell中直接执行他们之后ctrl+c会杀死当前进程?

经过一番查阅资料,发现这个其实和shell本身的机制是有关系的,我们知道,其实在shell中执行命令,本质上是shell fork一个子进程,之后调用exec运行输入的命令,其实在这个过程中shell fork之后就设置了对于SIGINT信号的处理方法,那就是Terminate,因此这就解释了为什么在前台运行sleep 或者ignore_sig的时候如果调用kill -2 或者ctrl+c是可以杀死他们的,而在脚本中放置在后台则不会(因为脚本后台运行不能算作是shell fork出来的)


参考链接

Signals by default are handled by the kernel. Old Unix systems had 15 signals; now they have more. You can check </usr/include/signal.h> (or kill -l). CTRL+C is the signal with name SIGINT.

The default action for handling each signal is defined in the kernel too, and usually it terminates the process that received the signal.

All signals (but SIGKILL) can be handled by program.

And this is what the shell does:

  • When the shell running in interactive mode, it has a special signal handling for this mode.

  • When you run a program, for example

1
2
>   find
>

, the shell:

  • forks itself
  • and for the child set the default signal handling
  • replace the child with the given command (e.g. with find)
  • when you press CTRL+C, parent shell handle this signal but the child will receive it - with the default action - terminate. (the child can implement signal handling too)

You can trap signals in your shell script too…

And you can set signal handling for your interactive shell too, try enter this at the top of you ~/.profile. (Ensure than you’re a already logged in and test it with another terminal - you can lock out yourself)

1
2
> trap 'echo "Dont do this"' 2
>

Now, every time you press CTRL+C in your shell, it will print a message. Don’t forget to remove the line!

If interested, you can check the plain old /bin/sh signal handling in the source code here.

At the above there were some misinformations in the comments (now deleted), so if someone interested here is a very nice link - how the signal handling works.


其他一些比较好的连接

  1. http://ajhaupt.blogspot.jp/2011/01/whats-difference-between-ctrl-c-and.html
  2. https://unix.stackexchange.com/questions/45426/why-would-ctrl-c-behave-differently-than-kill-2
  3. https://unix.stackexchange.com/questions/149741/why-is-sigint-not-propagated-to-child-process-when-sent-to-its-parent-process
  4. https://unix.stackexchange.com/questions/163561/control-which-process-gets-cancelled-by-ctrlc
  5. http://www.vidarholen.net/contents/blog/?p=34

转载请注明来源链接 http://just4fun.im/2017/10/22/信号与ctrl c/ 尊重知识,谢谢:)